#include "TileWorld.h"
#include "WorldDescriptor.h"
#include "TextureManager.h"
#include "corona.h"
#include "Logger.h"
#include "Exception.h"

using namespace game_engine;
using namespace game_engine::tile_engine;
using namespace game_engine::data_format;
using namespace game_engine::utils;

///////////////////////////////////////////////////////////////////////////////
// Constructors and Destructors
///////////////////////////////////////////////////////////////////////////////

TileWorld::TileWorld(void)
    : worldWidth(0), worldHeight(0), tileWidth(0), tileHeight(0)
{
}

TileWorld::~TileWorld(void)
{
    for (INT32 x = 0; x<worldWidth; x++)
    {
        for(INT32 y=0; y<worldHeight; y++)
        {
            if (tiles[x][y] != NULL)
            {
                delete tiles[x][y];
            }
        }
        if (tiles[x] != NULL)
        {
            delete tiles[x];
        }
    }

    if (tiles != NULL)
    {
        delete tiles;
    }
}


///////////////////////////////////////////////////////////////////////////////
// Other public methods
///////////////////////////////////////////////////////////////////////////////

void TileWorld::loadLevel(const string &levelFilename, 
                          const string &imageFilename) throw(...)
{

    // Parse the level descriptor file
    WorldDescriptor level(levelFilename);
    try
    {
        level.parse();
    } catch (exception &e) {
        string error("error loading level: ");
        throw RuntimeException(error + e.what());
    }

    // Load image file
    TextureManager *tm = TextureManager::getInstance();
    tm->setTexture(imageFilename, GL_RGB);    

    // Get level dimensions (in tiles)
    worldWidth = level.getWorldWidth();
    worldHeight = level.getWorldHeight();

    // Get tile's information
    tileWidth = level.getTileWidth();
    tileHeight = level.getTileHeight();

    // Allocate tiles
    allocateTiles();
    for (INT32 y=0; y<worldHeight; y++)
    {
        for (INT32 x=0; x<worldWidth; x++)
        {
            addTile(level.getTextureId(x, y), level.getPhysicsInfo(x, y),
                x, y, imageFilename);
        }
    }
}

void TileWorld::draw(const Point &cameraPosition, const Rect &view) const
{
    FLOAT32 cameraX = cameraPosition.getX();
    FLOAT32 cameraY = cameraPosition.getY();

    // FIXME : delete following lines (debugging purposes)
    if (cameraY == 520)
    {
        cameraX = cameraX;
    }

    FLOAT32 xoff = -cameraX + view.getX() + (view.getWidth() / 2);
	FLOAT32 yoff = -cameraY + view.getY() + (view.getHeight() / 2);
    Rect r(0.0f, 0.0f, tileHeight, tileWidth);

   	for(INT32 x=0; x<worldWidth; x++)
    {
        r.setX((x * tileWidth) + xoff);

   		// Optimization: don't draw offscreen tiles. Only useful when world is 
        // much larger than screen
		if(((r.getX() + r.getWidth()) < view.getX()) ||
		   (r.getX() > (view.getX() + view.getWidth())))
        {
			continue;
		}

  		for(INT32 y=0; y<worldHeight; y++)
        {
			r.setY((y * tileHeight) + yoff);
			if(((r.getY() + r.getHeight()) < view.getY()) ||
			   (r.getY() > (view.getY() + view.getHeight()))) 
            {
				continue;
			}
			tiles[x][y]->drawInRect(r);
		}
    } // for(INT32 x=0; x<worldWidth; x++)
}

const Tile* TileWorld::tileAt(const Point &worldPosition) const
{
    INT32 x;
    INT32 y;
    try
    {
        computeCoordinates(worldPosition, x, y);
    } catch (exception &e)
    {
        Logger::ERR(GAMEENGINE_LOG, e.what());
        return NULL;
    }

	return tiles[x][y];
}

//const BOOL TileWorld::walkable(const Point &position) const
//{
//    const Tile *overtile = tileAt(position);
//	return !(overtile == NULL || (overtile->getFlags() & UNWALKABLE) != 0);
//}

const vector<Tile*> TileWorld::surroundingTiles(const Point &worldPosition) const
{
    vector<Tile*> list;
    list.clear();

    // Get coordinates of current tile
    INT32 x;
    INT32 y;
    try
    {
        computeCoordinates(worldPosition, x, y);
    } catch (exception &e)
    {
        Logger::ERR(GAMEENGINE_LOG, e.what());
        return list;
    }

    // Add adjacent tiles to list if they exist
    // FIXME : there must be another way ...
    Rect r(0.0f, 0.0f, tileHeight, tileWidth);
    if (tileExists(x, y+1))     list.push_back(tiles[x][y+1]);
    else    list.push_back(new Tile(Point(x*tileWidth, (y+1)*tileHeight), r));
    if (tileExists(x+1, y+1))   list.push_back(tiles[x+1][y+1]);
    else    list.push_back(new Tile(Point((x+1)*tileWidth, (y+1)*tileHeight), r));
    if (tileExists(x+1, y))     list.push_back(tiles[x+1][y]);
    else    list.push_back(new Tile(Point((x+1)*tileWidth, y*tileHeight), r));
    if (tileExists(x+1, y-1))   list.push_back(tiles[x+1][y-1]);
    else    list.push_back(new Tile(Point((x+1)*tileWidth, (y-1)*tileHeight), r));
    if (tileExists(x, y-1))     list.push_back(tiles[x][y-1]);
    else    list.push_back(new Tile(Point(x*tileWidth, (y-1)*tileHeight), r));
    if (tileExists(x-1, y-1))   list.push_back(tiles[x-1][y-1]);
    else    list.push_back(new Tile(Point((x-1)*tileWidth, (y-1)*tileHeight), r));
    if (tileExists(x-1, y))     list.push_back(tiles[x-1][y]);
    else    list.push_back(new Tile(Point((x-1)*tileWidth, y*tileHeight), r));
    if (tileExists(x-1, y+1))   list.push_back(tiles[x-1][y+1]);
    else    list.push_back(new Tile(Point((x-1)*tileWidth, (y+1)*tileHeight), r));

    return list;
}

///////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////

void TileWorld::allocateTiles(void)
{
    tiles = new Tile**[worldWidth];
    for (INT32 x = 0; x<worldWidth; x++)
    {
        tiles[x] = new Tile*[worldHeight];
        for(INT32 y=0; y<worldHeight; y++)
        {
            tiles[x][y] = new Tile(Point(x * tileWidth, y * tileHeight));
        }
    }
}

void TileWorld::addTile(const INT32 idTile, const INT32 phys, const INT32 x, 
                        const INT32 y, const string &imageFilename)
{
    // Open image with Corona
    corona::Image* img = corona::OpenImage(imageFilename.c_str());
    if (img == NULL)
    {
        // throw exception
    }

    INT32 tx = idTile * static_cast<INT32>(tileWidth) % img->getWidth();
    INT32 row = ((idTile * static_cast<INT32>(tileHeight)) - tx) / img->getWidth();
    INT32 ty = row * tileHeight;
    Rect r(tx, ty, tileHeight, tileWidth);
    tiles[x][worldHeight - y - 1]->setTextureName(imageFilename);
    tiles[x][worldHeight - y - 1]->setFrame(r);
    tiles[x][worldHeight - y - 1]->setTileType(phys);
}

void TileWorld::computeCoordinates(const Point &worldPosition, INT32 &x, 
                                   INT32 &y) const throw (...)
{
	x = worldPosition.getX() / tileWidth; 
	y = worldPosition.getY() / tileHeight;

	// Note that x and y are slightly wrong when negative and near zero, 
    // because [-TILE_SIZE..TILESIZE]/TILESIZE == 0, even though [-TILESIZE..0)
    // would be considered out of bounds
	if(worldPosition.getX() < 0 || worldPosition.getY() < 0 || 
        x >= worldWidth || y >= worldHeight)
    {        
	    throw RuntimeException("tile outside of world dimensions");	
	}
}

const BOOL TileWorld::tileExists(const INT32 x, const INT32 y) const
{
    BOOL exists = false;
    if ((x >= 0) && (x < worldWidth) && (y >= 0) && (y < worldHeight))
    {
        exists = true;
    }
    return exists;
}

///////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////

const INT32 TileWorld::getWorldWidth(void) const
{
    return worldWidth;
}

const INT32 TileWorld::getWorldHeight(void) const
{
    return worldHeight;
}

const INT32 TileWorld::getTileWidth(void) const
{
	return tileWidth;
}

const INT32 TileWorld::getTileHeight(void) const
{
	return tileHeight;
}

///////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////

void TileWorld::setWorldWidth(const INT32 width)
{
    worldWidth = width;
}

void TileWorld::setWorldHeight(const INT32 heigth)
{
    worldHeight = heigth;
}
